package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
	"wisdom_education_in_kunshan/ocr"
	_ "wisdom_education_in_kunshan/ocr/manual"
	"wisdom_education_in_kunshan/znet"

	"github.com/360EntSecGroup-Skylar/excelize"
	"github.com/PuerkitoBio/goquery"
)

// ResultContent ...
var ResultContent = "777-1706-0-%2C778-1710-0-%2C779-1714-0-%2C780-1718-0-%2C781-1750-0-%2C782-1726-0-%2C783-1730-0-%2C784-1735-0-%2C785-1738-0-%2C786-1742-0-%2C787-1746-0-"

// CheckAnswerURL ...
var CheckAnswerURL = func(answer *goquery.Document) bool {
	// answer.Parent().Prev().Find("span").Text()  过滤掉 发布日期：2019/6/10 的答卷
	var tmp *goquery.Selection
	if tmp = answer.Parent(); tmp == nil {
		return true
	}
	if tmp = tmp.Prev(); tmp == nil {
		return true
	}
	if tmp = tmp.Find("span"); tmp == nil {
		return true
	}
	dataStr := tmp.Text()
	if strings.Contains(dataStr, "2019/6/10") {
		return false
	} else {
		return true
	}
}

const (
	// ErrorHTTP http未知异常
	ErrorHTTP = -1
	// ErrorNone 成功
	ErrorNone = 0
	// ErrorRequest 请求失败
	ErrorRequest = 1
	// ErrorCode 验证码失败
	ErrorCode = 33
)

type user struct {
	userName string
	userPwd  string
}

var processChan chan int

// ProcessNum 最大执行线程
const ProcessNum = 3

var ocrArg string
var wg sync.WaitGroup
var inited bool

func getParentDirectory(dirctory string) string {
	return dirctory[0:strings.LastIndex(dirctory, "/")]
}

func init() {
	os.Remove("tmp")
	ok, err := Mkdir("./tmp")
	inited = err == nil && ok
	if !inited {
		log.Println("tmp文件夹不存在,请手动创建", err)
	}
}

func main() {
	defer func() {
		for i := 5; i == 0; i-- {
			log.Println(fmt.Sprintf("倒数%d秒之后退出......", i))
			time.Sleep(time.Second)
		}
	}()
	if !inited {
		return
	}
	processChan = make(chan int, ProcessNum)
	for i := 0; i < ProcessNum; i++ {
		processChan <- 1
	}
	files, _ := ListDir(".", "xls")
	if files != nil {
		for _, file := range files {
			log.Println(fmt.Sprintf("当前程序不支持xls文件( %s ),请转成xlsx", file))
		}
	}
	files, err := ListDir(".", "xlsx")
	if err != nil {
		log.Println("寻找当前目录下的xlsx文件", err)
		return
	}
	users := make(map[string]string)
	for _, file := range files {
		readExcel(file, users)
	}
	// baidu ocr cookie
	baiduBuf, err := ioutil.ReadFile("baidu.cfg")
	if err != nil {
		ocrArg = ""
	} else {
		ocrArg = string(baiduBuf)
	}
	//
	ocr.EnableDebug(false)
	for userName, userPwd := range users {
		<-processChan
		wg.Add(1)
		go answer(userName, userPwd)
	}
	wg.Wait()
	log.Println("全部账号完成.")
	time.Sleep(time.Second)
}

func readExcel(file string, users map[string]string) {
	xlsx, err := excelize.OpenFile(file)
	if err != nil {
		log.Println(file, err)
		return
	}
	g1 := xlsx.GetCellValue("学生账号", "G1")
	if g1 == "" || len(g1) == 0 {
		log.Println(file, "不存在工作表[学生账号] 或者 此工作表不存在列[学生账号](请设置G1单元格'学生帐号',并依次填写学生账号)！")
		return
	}
	start := 2
	for {
		account := xlsx.GetCellValue("学生账号", fmt.Sprintf("G%d", start))
		account = strings.Trim(account, " ")
		if len(account) > 0 {
			users[account] = "123456ks"
			start++
		} else {
			return
		}
	}
}

func answer(userName, userPwd string) {
	tryCount := 0
	running := true
	for running {
		code := doAnswer(userName, userPwd)
		if code == ErrorNone {
			running = false
			writeLocalFile(fmt.Sprintf("完成:%s\t%s\n", userName, userPwd))
		} else {
			time.Sleep(time.Second)
			if tryCount < 3 {
				tryCount++
			} else {
				writeLocalFile(fmt.Sprintf("失败:%s\t%s\n", userName, userPwd))
				running = false
			}
		}
		// switch code {
		// case ErrorHTTP:
		// case ErrorNone:
		// 	running = false
		// case ErrorRequest:
		// 	if tryCount < 3 {
		// 		tryCount++
		// 	} else {
		// 		running = false
		// 	}
		// 	break
		// case ErrorCode:
		// 	time.Sleep(time.Second)
		// 	break
		// }
	}
	processChan <- 1
	wg.Done()
}

func doAnswer(userName, userPwd string) int {
	httpClient := znet.NewClient()
	jar, err := cookiejar.New(nil)
	if err != nil {
		log.Println(userName, "Init httpClient err", err)
		return ErrorHTTP
	}
	httpClient.Jar = jar
	httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
		return errors.New("Not Allow Redirect")
	}

	// goto index page and getCode
	params, ok := gotoLoginPage(httpClient, userName, userPwd)
	if !ok {
		log.Println(userName, "初始化登陆页面失败")
		return ErrorRequest
	}
	requestBody := fmt.Sprintf("__VIEWSTATE=%s&__VIEWSTATEGENERATOR=%s&__EVENTVALIDATION=%s&txtUsername=%s&txtPassword=%s&txtCode=%s&btnSignIn=%s", params[0], params[1], params[2], params[3], params[4], params[5], "%E7%99%BB+%E5%BD%95")
	// do login
	if !doLogin(httpClient, requestBody) {
		log.Println(userName, "登陆失败")
		return ErrorCode
	}
	// SSOAuth
	if !znet.Get(httpClient, "http://uia.ksedu.cn/SSOAuth.aspx") {
		log.Println(userName, "SSOAuth GET http://uia.ksedu.cn/SSOAuth.aspx err")
		return ErrorRequest
	}
	// getUserTokenInfo
	ssoContext := make([]string, 6)
	ssoContext[0] = params[0]
	ssoContext[1] = params[1]
	ssoContext[2] = params[2]
	userTokenInfo := getUserTokenInfo(httpClient)
	if userTokenInfo == nil || len(userTokenInfo) != 3 {
		log.Println(userName, "GetUserTokenInfo err", userTokenInfo)
		return ErrorRequest
	}
	ssoContext[3] = userTokenInfo[0]
	ssoContext[4] = userTokenInfo[1]
	ssoContext[5] = userTokenInfo[2]
	if !ssoAuth(httpClient, ssoContext) {
		log.Println(userName, "SSOAuth err")
		return ErrorRequest
	}

	// inStudentPage
	if !znet.Get(httpClient, "http://smart.ksedu.cn/home/SSOProcessing?sty=stu") {
		log.Println(userName, "InitStudentPage GET http://smart.ksedu.cn/home/SSOProcessing?sty=stu err")
		return ErrorRequest
	}
	if !znet.Get(httpClient, "http://sso.ksedu.cn/Authhandler.aspx?appcode=smartstu") {
		log.Println(userName, "InitStudentPage GET http://sso.ksedu.cn/Authhandler.aspx?appcode=smartstu err")
		return ErrorRequest
	}
	t := time.Now()
	url := fmt.Sprintf("http://smart.ksedu.cn/api/LoginAPI/GetSSOLoginUser?userguid=%s&sty=stu&_=%d", userTokenInfo[2], t.Unix())
	if !znet.Get(httpClient, url) {
		log.Println(userName, "InitStudentPage GET http://smart.ksedu.cn/api/LoginAPI/GetSSOLoginUser err")
		return ErrorRequest
	}
	if !znet.Get(httpClient, "http://smart.ksedu.cn/?sty=stu") {
		log.Println(userName, "InitStudentPage GET http://smart.ksedu.cn/?sty=stu err")
		return ErrorRequest
	}
	for tryCount := 0; tryCount < 3; tryCount++ {
		if getTeacherSurveyAndAnswer(httpClient) {
			return ErrorNone
		}
	}
	return ErrorRequest
}

func downCode(path string, httpClient *http.Client) bool {
	rp := rand.Intn(65535)
	downCodeURL := fmt.Sprintf("http://sso.ksedu.cn/CodeImage.aspx?%d", rp)
	req, _ := http.NewRequest("GET", downCodeURL, nil)
	req.Header.Add("Connection", "keep-alive")
	req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")
	res, err := httpClient.Do(req)
	if err != nil {
		log.Println("Get http://sso.ksedu.cn/CodeImage.aspx err!", err)
		return false
	}
	defer res.Body.Close()
	err = SaveFile(path, res.Body)
	if err != nil {
		log.Println("Save CodeImage err", err)
		return false
	}
	return true
}

func getLoginCode(httpClient *http.Client) (string, bool) {
	nt := time.Now()
	codeDownLoadPath := fmt.Sprintf("./tmp/code_%02d%02d%02d%02d%02d%02d%05d.png", nt.Year(), nt.Month(), nt.Day(), nt.Hour(), nt.Minute(), nt.Second(), (rand.Intn(10000) + 1000))
	codeDownLoadPath, err := filepath.Abs(codeDownLoadPath)
	if err != nil {
		return ocr.Null, false
	}
	defer os.Remove(codeDownLoadPath)
	if !downCode(codeDownLoadPath, httpClient) {
		log.Println("下载验证码失败")
		return ocr.Null, false
	}
	codeStr, ok := ocr.OCR(codeDownLoadPath, ocrArg)
	if !ok {
		log.Println("验证码识别失败")
		return ocr.Null, false
	}
	code := []rune(codeStr)
	index := 0
	for i := 0; i < len(code); i++ {
		ci := code[i]
		if (ci >= 48 && ci <= 57) || (ci >= 65 && ci <= 90) || ci >= 97 && ci <= 122 {
			switch ci {
			case 79:
			case 111:
				code[index] = 48
				break
			case 103:
				code[index] = 57
				break
			default:
				code[index] = ci
				break
			}
			index++
		}
	}
	if index == 4 {
		return string(code[0:index]), true
	} else {
		log.Println(fmt.Sprintf("验证码识别失败(%d)\n\t%s", index, codeStr))
		return ocr.Null, false
	}
}

func gotoLoginPage(httpClient *http.Client, userName, userPwd string) ([]string, bool) {
	req, _ := http.NewRequest("GET", "http://sso.ksedu.cn/SignIn.aspx", nil)
	req.Header.Add("Connection", "keep-alive")
	req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")
	req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
	req.Header.Add("Accept-Encoding", "gzip, deflate")
	req.Header.Add("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2")
	res, err := httpClient.Do(req)
	if err != nil {
		log.Println("GET http://sso.ksedu.cn/SignIn.aspx err!", err)
		return nil, false
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		log.Println("GET SignIn.aspx err", res.StatusCode, res.Status)
		return nil, false
	}
	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		log.Println("NewDocumentFromReader SignIn.aspx err!", err)
		return nil, false
	}
	params := make([]string, 6)
	params[0], _ = doc.Find("#__VIEWSTATE").Attr("value")
	params[0] = url.QueryEscape(params[0])
	params[1], _ = doc.Find("#__VIEWSTATEGENERATOR").Attr("value")
	params[1] = url.QueryEscape(params[1])
	params[2], _ = doc.Find("#__EVENTVALIDATION").Attr("value")
	params[2] = url.QueryEscape(params[2])

	code, ok := getLoginCode(httpClient)
	if !ok {
		return nil, false
	}
	params[3] = userName
	params[4] = userPwd
	params[5] = code
	return params, true
}

func doLogin(httpClient *http.Client, requestBody string) bool {
	req, _ := http.NewRequest("POST", "http://sso.ksedu.cn/SignIn.aspx", strings.NewReader(requestBody))
	req.Header.Add("Connection", "keep-alive")
	req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")
	req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
	req.Header.Add("Accept-Encoding", "gzip, deflate")
	req.Header.Add("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2")
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	res, err := httpClient.Do(req)
	if err != nil && res == nil {
		log.Println("POST http://sso.ksedu.cn/SignIn.aspx err", err)
		return false
	}
	defer res.Body.Close()
	switch res.StatusCode {
	case 301:
	case 302:
		return true
	default:
		log.Println("Do login err", res.StatusCode, res.Status)
		return false
	}
	return false
}

func getUserTokenInfo(httpClient *http.Client) []string {
	res, err := httpClient.Get("http://sso.ksedu.cn/Authhandler.aspx?appcode=portal")
	if err != nil {
		log.Println("Get http://sso.ksedu.cn/Authhandler.aspx?appcode=portal err", err)
		return nil
	}
	defer res.Body.Close()
	ssoInfoBuf, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Println("ReadUserTokenInfo err", err)
		return nil
	}
	SSOInfo := strings.Replace(string(ssoInfoBuf), " ", "", -1)
	index := strings.Index(SSOInfo, "newSSOContext")
	SSOInfoRunes := []rune(SSOInfo)
	SSOInfo = string(SSOInfoRunes[index:len(SSOInfoRunes)])
	end := strings.Index(SSOInfo, ");")
	SSOInfo = string(SSOInfoRunes[index+14 : index+end])
	SSOInfo = strings.Replace(SSOInfo, "'", "", -1)
	SSOInfo = strings.Replace(SSOInfo, "\"", "", -1)
	return strings.Split(SSOInfo, ",")
}

func ssoAuth(httpClient *http.Client, ssoContext []string) bool {
	requestBody := fmt.Sprintf("__VIEWSTATE=%s&__VIEWSTATEGENERATOR=%s&__EVENTVALIDATION=%s&sso_signinurl=%s&sso_signouturl=%s&sso_tokeninfo=%s&isSubmit=1", ssoContext[0], ssoContext[1], ssoContext[2], ssoContext[3], ssoContext[4], ssoContext[5])
	req, _ := http.NewRequest("POST", "http://uia.ksedu.cn/SSOAuth.aspx", strings.NewReader(requestBody))
	req.Header.Add("Connection", "keep-alive")
	req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")
	req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
	req.Header.Add("Accept-Encoding", "gzip, deflate")
	req.Header.Add("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2")
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	res, err := httpClient.Do(req)
	if err != nil && res == nil {
		log.Println("POST http://uia.ksedu.cn/SSOAuth.aspx err", err)
		return false
	}
	defer res.Body.Close()
	switch res.StatusCode {
	case 301:
	case 302:
		return true
	default:
		log.Println("SSOAuth err", res.StatusCode, res.Status)
		return false
	}
	return false
}

func getTeacherSurveyAndAnswer(httpClient *http.Client) bool {
	req, _ := http.NewRequest("GET", "http://smart.ksedu.cn/Space/StudentSkipPage", nil)
	res, err := httpClient.Do(req)
	if err != nil {
		log.Println("GET http://smart.ksedu.cn/Space/StudentSkipPage err", err)
		return false
	}
	defer res.Body.Close()
	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		log.Println("Read document err", err)
		return false
	}
	script := doc.Find("script").Text()
	// 	script := `
	// 	window.$ = $j11;
	// 	window.jQuery = $j11;
	// $(function () {
	// 	var url = "http://ps.ksedu.cn/TeacherSurvey/TeachingSurveyStudents?inputerId=16C699E80C7BB7655E87FB9533681A99&amp;inputerName=03958692D476BD2F&amp;teacherIds=9BA31773A9EDE669E5051667C71E259354D6B27B9283542E54D6B27B9283542E54D6B27B9283542E54D6B27B9283542E54D6B27B9283542E54D6B27B9283542E3E7020B921CED501634B58D99DEA0B8A232F5D196210F0AE54D6B27B9283542E54D6B27B9283542E54D6B27B9283542E54D6B27B9283542E54D6B27B9283542E55A5B596C0FDC41799768E0DE121D2150C806CDFAEDB9D9B4CF741F9ACC375AEDC2820F9C0563CA84CF741F9ACC375AE1DD4913641DC6C504CF741F9ACC375AE7531D92E54F446FF4CF741F9ACC375AEA0470B1F82652E493DE3057B515B509DEB4ACCA51A509E113DE3057B515B509DBFFA732662AD4F7B";
	// 	url = url.replace(new RegExp(/(&amp;)/g), '&');
	// 	window.location.href = url;
	// })`
	script = strings.Replace(script, " ", "", -1)
	index := strings.Index(script, "http://ps.ksedu.cn/TeacherSurvey/TeachingSurveyStudents?")
	scriptRunes := []rune(script)
	scriptRunes = scriptRunes[index:len(scriptRunes)]
	script = string(scriptRunes)
	index = strings.Index(script, "\"")
	script = string(scriptRunes[56:index])
	script = strings.Replace(script, "&amp;", "&", -1)
	//
	pageIndex := 1
	findAnswerURL := true
	answerAllResult := true
	for findAnswerURL {
		requestURL := fmt.Sprintf("http://ps.ksedu.cn/TeacherSurvey/TeachingSurveyStudentsData?pageSize=50&pageIndex=%d&%s", pageIndex, script)
		req, _ := http.NewRequest("GET", requestURL, nil)
		res, err := httpClient.Do(req)
		if err != nil {
			log.Println("GET http://ps.ksedu.cn/TeacherSurvey/TeachingSurveyStudentsData  pageIndex", pageIndex, err)
			return false
		}
		doc, err := goquery.NewDocumentFromReader(res.Body)
		res.Body.Close()
		if err != nil {
			log.Println("Document TeachingSurveyStudentsData err", err)
			return false
		}
		answers := doc.Find(".answer-1")
		if answers.Size() < 50 {
			findAnswerURL = false
		} else {
			pageIndex++
		}
		for i := 0; i < answers.Size(); i++ {
			answer := goquery.NewDocumentFromNode(answers.Get(i))
			if CheckAnswerURL(answer) {
				answerURL, exist := answer.Attr("href")
				if exist {
					answerURL = fmt.Sprintf("http://ps.ksedu.cn%s", answerURL)
					if !answerQuestion(httpClient, answerURL) && answerAllResult {
						answerAllResult = false
					}
				}
			}
		}
	}
	return answerAllResult
}

func answerQuestion(httpClient *http.Client, url string) bool {
	req, _ := http.NewRequest("GET", url, nil)
	res, err := httpClient.Do(req)
	if err != nil {
		log.Println("GET ", url, err)
		return false
	}
	defer res.Body.Close()
	doc, err := goquery.NewDocumentFromReader(res.Body)
	title := doc.Find("h2")
	surveyRelationID, _ := title.Attr("id")
	inputerID, _ := doc.Find("#user").Attr("value")
	questions := ResultContent
	requestBody := "RelationId=%s&InputerId=%s&InputerType=%d&ResultContent=%s"
	requestBody = fmt.Sprintf(requestBody, surveyRelationID, inputerID, 2, questions)
	req, _ = http.NewRequest("POST", "http://ps.ksedu.cn/api/TeachingSurveyAPI/SaveTeacherSurvey", strings.NewReader(requestBody))
	req.Header.Add("Connection", "keep-alive")
	req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")
	req.Header.Add("Accept", "application/json, text/javascript, */*; q=0.01")
	req.Header.Add("Accept-Encoding", "gzip, deflate")
	req.Header.Add("Accept-Language", "zh-CN,zh;q=0.8")
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
	req.Header.Add("X-Requested-With", "XMLHttpRequest")
	answerRes, err := httpClient.Do(req)
	if err != nil {
		log.Println("POST http://ps.ksedu.cn/api/TeachingSurveyAPI/SaveTeacherSurvey err", err)
		return false
	}
	defer answerRes.Body.Close()
	ar := &AnswerJSON{}
	jsonStrBuf, err := ioutil.ReadAll(answerRes.Body)
	if err != nil {
		log.Println("答题失败(ioutil.ReadAll-->answerRes.Body)", err)
		return false
	}
	ar.ParseJSON(string(jsonStrBuf))
	if !ar.Result {
		log.Println("答题失败", ar.Msg)
		return false
	}
	return true
}

// AnswerJSON 答题json
type AnswerJSON struct {
	Result bool   `json:"result"`
	Msg    string `json:"msg"`
}

// ParseJSON json parse
func (a *AnswerJSON) ParseJSON(jsonStr string) {
	jsonStart := strings.Index(jsonStr, "{")
	jsonEnd := strings.LastIndex(jsonStr, "}")
	jsonStr = jsonStr[jsonStart : jsonEnd+1]
	jsonStr = strings.Replace(jsonStr, "\\\"", "\"", -1)
	decoder := json.NewDecoder(strings.NewReader(jsonStr))
	err := decoder.Decode(a)
	if err != nil {
		a.Result = false
		a.Msg = err.Error()
	}
}
